Together with the TreeView control, the ListView control has been made popular by Windows Explorer. Now many Windows applications use this pair of controls side by side, and they're therefore called Windows Explorer-like applications. In these applications, the end user selects a Node in the TreeView control on the left and sees some information related to it in the rightmost ListView control.
The ListView control supports four basic view modes: Icon, SmallIcon, List, and Report. To see how each mode is rendered, try the corresponding items in the Windows Explorer View menu. (The Report mode corresponds to the Details menu command.) To give you an idea of the flexibility of this control, you should know that the Windows desktop is nothing but a large ListView control in Icon mode with a transparent background. When used in Report mode, the ListView control resembles a grid control and lets you display well-organized information about each item.
The Visual Basic 6 version of the ListView control has many new features. It can display icons in column headers and grid cells; it supports hot tracking, full row selection, and reordering of columns; and its items can have independent Bold and Color attributes. The new ListView control can also show a background bitmap, grid lines, and check boxes beside each item.
The ListView control exposes two distinct collections: The ListItems collection comprises individual ListItem objects, each one corresponding to an item in the control, whereas the ColumnHeaders collection includes ColumnHeader objects that affect the appearance of the individual headers visible in Report mode. A third collection, ListSubItems, contains data for all the cells displayed in Report mode.
While you can use the regular Properties window to set most properties of a ListView control, it's surely preferable to use a ListView control's custom Properties dialog box, shown in Figure 10-11.
Figure 10-11. The General tab of the Properties dialog box of a ListView control.
I've already referred to the View property, which can be one of the following values: 0-lvwIcon, 1-lvwSmallIcon, 2-lvwList, or 3-lvwReport. You can change this property at run time as well as let the user change it (typically by offering four options in the View menu of your application). The Arrange property lets you decide whether icons are automatically aligned to the left of the control (1-lvwAutoLeft) or to the top of the control (2-lvwAutoTop), or whether they shouldn't be aligned at all (0-lvwNone, the default behavior). This property takes effect only when the control is in Icon or SmallIcon display mode.
The LabelEdit property determines whether the user can edit the text associated with an item in the control. If this property is set to 0-lvwAutomatic, the edit operation can be initiated only by code using a StartLabelEdit method. The LabelWrap Boolean property specifies whether longer labels wrap on multiple lines of text when in Icon mode. The HideColumnHeaders Boolean property determines whether column headers are visible when in Report mode. (The default value is False, which makes the columns visible.) If you assign the MultiSelect property the True value, the ListView control behaves much like a ListBox control whose MultiSelect property has been set to 2-Extended.
A few properties are new to Visual Basic 6. If you set AllowColumnReorder to True, users can reorder columns by dragging their headers when the control is in Report mode. You can change the appearance of the ListView control by setting the GridLines property to True (thus adding horizontal and vertical lines). The third new property designed to change the appearance of the ListView control, the FlatScrollBar property, seems to be buggy: If you set it to True, the scroll bars don't show. The ListView control shares a few new properties with the TreeView control. I've already described the Checkboxes property (which lets you display a check box beside each item) and the FullRowSelect property (for highlighting entire rows instead of a row's first item only). The HotTracking Boolean property, if True, changes the appearance of an item when the user moves the mouse cursor over it. The HoverSelection Boolean property, if True, lets you select an item by simply moving the mouse cursor over it. See Figure 10-12 for an example of what you can get with these new properties.
Figure 10-12. A gallery of new features of ListView controls: Check boxes, grid lines, and Bold and ForeColor properties for individual items. The alternate row effect is achieved by means of a tiled background picture.
You can associate up to three ImageList subsidiaries with a ListView control: The first ImageList is used when the control is in Icon mode, the second is used when the control is in any other display mode, and the third is used for icons in column headers. You can set these associated ImageList controls at design time in the Image Lists tab of the Properties dialog box, or you can set them at run time by assigning an ImageList control to the ListView's Icons, SmallIcons, and ColumnHeaderIcons properties, respectively.
The ColumnHeaders property is new to Visual Basic 6 because previous versions of the ListView control didn't support icons in column headers:
' You can use the same ImageList control for different properties. Set ListView1.Icons = ImageList1 Set ListView1.SmallIcons = ImageList2 Set ListView1.ColumnHeaderIcons = ImageList2 |
You can automatically sort the items in the ListView control by setting a few properties in the Sorting tab of the Properties dialog box. Set the Sorted property to True if you want to sort items. SortKey is the index of the column that will be used for sorting (0 for the first column), and SortOrder is the sorting order (0-lvwAscending or 1-lvwDescending). You can also set these properties at run time.
You can create one or more ColumnHeader objects at design time by using the Column Header tab of the Properties dialog box. You just have to click on the Insert Column button and then type the values of the Text property (which will be displayed in the header), the Alignment property (Left, Right, or Center, although the first column header can only be left-aligned), and the Width in twips. You can also specify a value for the Key and Tag properties and set the index of the icon to be used for this header. (It's an index referred to by the ColumnHeaderIcons property in the ImageList control, or it's 0 if this column header doesn't have any icons.)
The ListView control that comes with Visual Basic 6 supports a background bitmap. You can load an image into the control at design time by using the Picture tab of the Properties dialog box and then selecting the Picture property in the leftmost list box. You can load an image in any format supported by the PictureBox control. Two additional properties affect how a background image is displayed in the control, but you can set them only in the regular Properties window. The PictureAlignment property lets you align the image in one of the four corners of the control, center it, or tile it to spread over the entire control's internal area. The TextBackground property determines whether the background of ListView's items is transparent (0lvwTransparent, the default value) or not (1-lvwOpaque); in the latter case, the background image will be visible only in the area not occupied by ListItem objects.
The background image offers a great method for displaying rows with alternate background colors, as shown in Figure 10-12. All you have to do is create a bitmap as tall as two rows and then set PictureAlignment = 5-lvwTile and TextBackground = 0-lvwTransparent.
While you can define the appearance of a ListView control at design time, you can fill it with data only through code. In this section, I'll show how to add and manipulate data for this control.
You add new items to the ListView controls with the ListItems collection's Add method, which has the following syntax:
Add([Index], [Key], [Text], [Icon], [SmallIcon]) As ListItem |
Index is the position at which you place the new item. (If you omit Index, the item is added to the end of the collection.) Key is the inserted item's optional key in the ListItems collection, Text is the string displayed in the control, Icon is an index or a key in the ImageList control pointed to by the Icons property, and SmallIcon is an index or a key in the ImageList control pointed to by the SmallIcons property. All these arguments are optional.
The Add method returns a reference to the ListItem object being created, which you can use to set those properties whose values can't be passed to the Add method itself, as in the following example:
' Create a new item with a "ghosted" appearance. Dim li As ListItem Set li = ListView1.ListItems.Add(, , "First item", 1) li.Ghosted = True |
ListItem objects support a number of new properties. The Bold and ForeColor properties affect the boldface and color attributes of the objects. The ToolTipText property allows you to define a different ToolTip for each item, and the Checked property sets or returns the state of the check box beside the item (if the ListView's Checkboxes property is True). When you have to assign multiple properties, you can use a With clause with the Add method:
With ListView1.ListItems.Add(, , "John Ross", 1) .Bold = True .ForeColor = vbRed .ToolTipText = "Manager of the Sales Dept." End With |
When working with ListView controls whose MultiSelect property is True, the user can select multiple items by clicking on them while pressing the Ctrl or the Shift key. You can modify the selection state of a ListItem object via code by assigning the appropriate value to the Selected property. With such ListView controls, you must also assign the SelectedItem property to make a ListItem the current item:
' Make the first ListItem object the current one. Set ListView1.SelectedItem = ListView1.ListItems(1) ' Select it. ListView1.ListItems(1).Selected = True |
Often you don't know at design time what columns should be displayed in a ListView control. For example, you might be showing the result of a user-defined query, in which case you don't know the number and the names of the fields involved. In such circumstances, you must create ColumnHeader objects at run time with the Add method of the ColumnHeaders collection, which has this syntax:
Add([Index], [Key], [Text], [Width], [Alignment], [Icon]) _ As ColumnHeader |
Index is the position in the collection, Key is an optional key, Text is the string displayed in the header, and Width is the column's width in twips. Alignment is one of the following constants: 0-lvwColumnLeft, 1-lvwColumnRight, or 2-lvwColumnCenter. Icon is an index or a key in the ListImage control referenced by the ColumnHeaderIcons property. With the exception of the Tag property, these are the only properties that can be assigned when a ColumnHeader object is created, so you can usually discard the return value of the Add method:
' Clear any existing column header. ListView1.ColumnHeaders.Clear ' The alignment for the first column header must be lvwColumnLeft. ListView1.ColumnHeaders.Add , , "Last Name", 2000, lvwColumnLeft ListView1.ColumnHeaders.Add , , "First Name", 2000, lvwColumnLeft ListView1.ColumnHeaders.Add , , "Salary", 1500, lvwColumnRight |
Each ListItem object supports a ListSubItems collection, which lets you create values displayed in the same row as the main ListItem object when the control is in Report mode. This collection replaces the SubItems array that was present in previous versions of the control. (The array is still supported for backward compatibility.) You can create new ListSubItem objects using the Add method of the ListSubItems collection:
Add([Index], [Key], [Text], [ReportIcon], [ToolTipText]) _ As ListSubItem |
Index is the position in the collection of the new item, Key is its optional key, Text is the string that will be displayed in the grid cell, ReportIcon is the index or the key of an icon in the ImageList control referenced by the SmallIcons property, and ToolTipText is the text of a ToolTip that appears when the user keeps the mouse hovering over this item. You can also assign individual Bold and ForeColor attributes to each ListSubItem:
' This ListItem goes under ColumnHeader(1). With ListView1.ListItems.Add(, , "Ross", 1) .Bold = True ' This ListSubItem goes under ColumnHeader(2). With .ListSubItems.Add(, , "John") .Bold = True End With ' This ListSubItem goes under ColumnHeader(3). With .ListSubItems.Add(, , "80,000") .Bold = True .ForeColor = vbRed End With End With |
ListSubItem objects are actually displayed only if the ListView control is in Report mode and only if there are enough ColumnHeader objects. For example, if the ColumnHeaders collection includes only three elements, the ListView control will display only up to three items in each row. Because the leftmost ColumnHeader object is located above ListItem elements, only the first two elements in the ListSubItems collection will be visible.
ListSubItem objects also support the Tag property, which you can use to store additional information associated with the items.
The ListView control can't be automatically bound to a database through Data, RemoteData, or an ADO Data control. In other words, if you want to load database data into this control you're on your own. The task of filling a ListView control with data read from a recordset isn't conceptually difficult, but you have to account for a few details. First you must retrieve the list of fields contained in the recordset and create a corresponding number of ColumnHeader objects of a suitable width. You also have to discard fields that can't be displayed in ListView controls (for example, BLOB fields), and you must determine the best alignment for each field (to the right for numbers and dates, to the left for all others). A routine that does all this, which you can easily reuse in your applications, is shown below.
Sub LoadListViewFromRecordset(LV As ListView, rs As ADODB.Recordset, _ Optional MaxRecords As Long) Dim fld As ADODB.Field, alignment As Integer Dim recCount As Long, i As Long, fldName As String Dim li As ListItem ' Clear the contents of the ListView control. LV.ListItems.Clear LV.ColumnHeaders.Clear ' Create the ColumnHeader collection. For Each fld In rs.Fields ' Filter out undesired field types. Select Case fld.Type Case adBoolean, adCurrency, adDate, adDecimal, adDouble alignment = lvwColumnRight Case adInteger, adNumeric, adSingle, adSmallInt, adVarNumeric alignment = lvwColumnRight Case adBSTR, adChar, adVarChar, adVariant alignment = lvwColumnLeft Case Else alignment = -1 ' This means "Unsupported field type". End Select ' If field type is OK, create a column with the correct alignment. If alignment <> -1 Then ' The first column must be left-aligned. If LV.ColumnHeaders.Count = 0 Then alignment = lvwColumnLeft LV.ColumnHeaders.Add , , fld.Name, fld.DefinedSize * 200, _ alignment End If Next ' Exit if there are no fields that can be shown. If LV.ColumnHeaders.Count = 0 Then Exit Sub ' Add all the records in the recordset. rs.MoveFirst Do Until rs.EOF recCount = recCount + 1 ' Add the main ListItem object. fldName = LV.ColumnHeaders(1).Text Set li = LV.ListItems.Add(, , rs.Fields(fldName) & "") ' Add all subsequent ListSubItem objects. For i = 2 To LV.ColumnHeaders.Count fldName = LV.ColumnHeaders(i) li.ListSubItems.Add , , rs.Fields(fldName) & "" Next If recCount = MaxRecords Then Exit Do rs.MoveNext Loop End Sub |
The LoadListViewFromRecordset routine expects an ADO Recordset and an optional MaxRecords argument that lets you limit the number of records displayed. This is necessary, because—as opposed to what happens with bound controls, which load only the information that is actually displayed—this routine reads all the rows in the recordset, which might be a lengthy process. I suggest that you set MaxRecords to 100 or 200, depending on the type of connection you have to your database and the speed of your CPU.
Another problem you face when loading data from a database is that you might need to manually adjust the width of each column. The LoadListViewFromRecordset routine initializes the width of all ColumnHeader objects using the fields' maximum width, but in most cases values stored in database fields are considerably shorter than this value. Instead of leaving the burden of the manual resizing on your users, you can change all columns' width programmatically using the following routine:
Sub ListViewAdjustColumnWidth(LV As ListView, _ Optional AccountForHeaders As Boolean) Dim row As Long, col As Long Dim width As Single, maxWidth As Single Dim saveFont As StdFont, saveScaleMode As Integer, cellText As String ' Exit if there aren't any items. If LV.ListItems.Count = 0 Then Exit Sub ' Save the font used by the parent form, and enforce ListView's ' font. (We need this in order to use the form's TextWidth ' method.) Set saveFont = LV.Parent.Font Set LV.Parent.Font = LV.Font ' Enforce ScaleMode = vbTwips for the parent. saveScaleMode = LV.Parent.ScaleMode LV.Parent.ScaleMode = vbTwips For col = 1 To LV.ColumnHeaders.Count maxWidth = 0 If AccountForHeaders Then maxWidth = LV.Parent.TextWidth(LV.ColumnHeaders(col).Text)+200 End If For row = 1 To LV.ListItems.Count ' Retrieve the text string from ListItems or ListSubItems. If col = 1 Then cellText = LV.ListItems(row).Text Else cellText = LV.ListItems(row).ListSubItems(col - 1).Text End If ' Calculate its width, and account for margins. ' Note: doesn't account for multiple-line text fields. width = LV.Parent.TextWidth(cellText) + 200 ' Update maxWidth if we've found a larger string. If width > maxWidth Then maxWidth = width Next ' Change the column's width. LV.ColumnHeaders(col).width = maxWidth Next ' Restore parent form's properties. Set LV.Parent.Font = saveFont LV.Parent.ScaleMode = saveScaleMode End Sub |
To determine the optimal width of all the values stored in a given column, the ListViewAdjustColumnWidth routine evaluates the maximum width of all the strings stored in that column. The problem is that the ListView control doesn't support the TextWidth method, so the routine relies on the TextWidth method exposed by the control's parent form. If a True value is passed in the second argument, the routine also accounts for the Text property of all ColumnHeader objects, so no header title is truncated.
The ListView control already allows you to automatically resize columns to fit their contents, even though this capability hasn't been exposed in the Visual Basic ActiveX control. In fact, you can interactively resize a column to fit the longest item it contains by double-clicking on its right border in the column header (as you would in the Details view mode of Windows Explorer). In the demonstration program on the companion CD, you'll find another version of the ListViewAdjustColumnWidth routine that does the resizing by using API calls instead of plain Visual Basic code. The following code sample shows how to use the ListViewAdjustColumnWidth routine to display all the records in the Orders table of the NorthWind.Mdb database, as shown in Figure 10-13:
Private Sub Form_Load() Dim cn As New ADODB.Connection, rs As New ADODB.Recordset ' WARNING: you might need to modify the DB path in the next line. cn.Open "Provider=Microsoft.Jet.OLEDB.3.51;" _ & "Data Source=C:\VisStudio\VB98\NWind.mdb" rs.Open "Orders", cn, adOpenForwardOnly, adLockReadOnly LoadListViewFromRecordset ListView1, rs ListViewAdjustColumnWidth ListView1, True End Sub |
On my 233-MHz machine, this code takes about 15 seconds to complete, which is more than most customers are willing to wait. Therefore, you should use this technique judiciously and set an upper limit to the number of records that are read from a database.
Figure 10-13. This demonstration program loads NorthWind's Orders table into a ListView control and lets you sort on any field by clicking on the corresponding column's header.
I already explained how you can define a sort key and a sort order at design time. You can get the same effect at run time by setting the Sorted, SortKey, and SortOrder properties. Usually you do this when the end user clicks on a column header, an action that you can trap in the ColumnClick event:
Private Sub ListView1_ColumnClick(ByVal ColumnHeader As _ MSComctlLib.ColumnHeader) ListView1.SortKey = ColumnHeader.Index - 1 ListView1.Sorted = True End Sub |
Things are slightly more complicated if you want to offer the ability to sort in either direction: The first click sorts in ascending order, and the second click sorts in descending order. In this case, you must check to see whether the column being clicked is already sorted:
Private Sub ListView1_ColumnClick(ByVal ColumnHeader As _ MSComctlLib.ColumnHeader) ' Sort according to data in this column. If ListView1.Sorted And _ ColumnHeader.Index - 1 = ListView1.SortKey Then ' Already sorted on this column, just invert the sort order. ListView1.SortOrder = 1 - ListView1.SortOrder Else ListView1.SortOrder = lvwAscending ListView1.SortKey = ColumnHeader.Index - 1 End If ListView1.Sorted = True End Sub |
The ListView control is able to sort string data exclusively. If you want to sort on columns that hold numeric or date information, you must resort to a trick. Create a new ColumnHeader object, fill it with string data derived from the numbers or dates you want to sort on, sort on that column, and finally delete those items. Here's a reusable routine that does all this for you:
Sub ListViewSortOnNonStringField(LV As ListView, ByVal ColumnIndex As _ Integer, SortOrder As Integer, Optional IsDateValue As Boolean) Dim li As ListItem, number As Double, newIndex As Integer ' This speeds up things by a factor of 10 or more. LV.Visible = False LV.Sorted = False ' Create a new, hidden field. LV.ColumnHeaders.Add , , "dummy column", 1 newIndex = LV.ColumnHeaders.Count - 1 For Each li In LV.ListItems ' Extract a number from the field. If IsDateValue Then number = DateValue(li.ListSubItems(ColumnIndex - 1)) Else number = CDbl(li.ListSubItems(ColumnIndex - 1)) End If ' Add a string that can be sorted using the Sorted property. li.ListSubItems.Add , , Format$(number, "000000000000000.000") Next ' Sort on this hidden field. LV.SortKey = newIndex LV.SortOrder = SortOrder LV.Sorted = True ' Remove data from the hidden column. LV.ColumnHeaders.Remove newIndex + 1 For Each li In LV.ListItems li.ListSubItems.Remove newIndex Next LV.Visible = True End Sub |
You can use the ListViewSortOnNonStringField routine from a ColumnClick event procedure, as I explained previously. The code I just showed you doesn't work with negative values, but the complete version on the companion CD solves this problem.
TIP
When the Sorted property is True, insert and remove operations are unbearably long. It's much better to set the Sorted property to False, do whatever updates you want, and then reset it to True. Depending on how many items are in the ListView control, you can easily speed up your routines by an order of magnitude.
Columns can be moved and reordered at run time. You can let the user drag a column to a new position by setting the AllowColumnReorder property to True. However, you shouldn't do this when your ListView control has the property Checkboxes set to True. If the user moves the first column, the control's contents will look pretty unusual because the check boxes will move with the first column.
Reordering columns from your code ensures better control over which columns are moved and where. In this case, you only have to assign a new value to the Position property of a ColumnHeader object. For example, you can exchange the position of the first two columns with this code:
ListView1.ColumnHeaders(1).Position = ListView1.ColumnHeaders(1).Position _ + 1 ' You need to refresh after reordering one or more columns. ListView1.Refresh |
You can quickly search for a string in a ListView control using the FindItem method, which has this syntax:
FindItem(Search, [Where], [Start], [Match]) As ListItem |
Search is the string being searched. Where specifies in which property the string will be searched: 0-lvwText for the Text property of ListItem objects, 1-lvwSubItem for the Text property of ListSubItem objects, or 2-lvwTag for the Tag property of ListItem objects. Start is the index or the key of the ListItem object from which the search begins. Match can be 0-lvwWholeWord or 1-lvwPartial and defines whether an item that begins with the searched string makes for a successful search. (Match can be used only if Where = 0-lvwText.)
Note that you can't search in the Tag property of ListSubItem objects, nor can you restrict the search to a single column of ListSubItems. All search operations are case insensitive.
The ListView control supports properties, methods, and events that are similar to those exposed by the TreeView control, so I won't describe them in detail here.
You can control when the user edits a value in the control using the BeforeLabelEdit and AfterLabelEdit events. Regardless of where the user clicks on the row, the only item that can actually be edited is the one in the leftmost column. If you want to programmatically start an edit operation, you have to make a given ListItem object the selected one and then invoke the StartLabelEdit method:
ListView1.SetFocus Set ListView1.SelectedItem = ListView1.ListItems(1) ListView1.StartLabelEdit |
If the control's Checkboxes property is set to True, you can read and modify the checked state of each row through the Checked property of individual ListItem objects. You can trap the action of ticking a check box by writing code in the ItemCheck event procedure. Similarly, the ItemClick event fires when a ListItem object is clicked.
ListItem objects expose an EnsureVisible method that, if necessary, scrolls the contents of the control to move the item into the visible area of the control. You can also query the ListView's GetFirstVisible method, which returns a reference to the first visible ListItem object.
The ListView's HitTest method returns the ListItem object at the specified coordinates. You typically use this method in drag-and-drop operations together with the DropHighlight property, as I explained in the section devoted to the TreeView control. By the way, there's no simple way to determine which column the mouse is on when the control is in Report mode.